import hashlib
import time
import json
from test_wallet import get_pubkey, verify_message
import traceback

award_amount=233

class Block:

    def __init__(self, index, proof_no, prev_hash, data, timestamp=None):
        self.index = index
        self.proof_no = proof_no
        self.prev_hash = prev_hash
        self.data = data
        self.timestamp = timestamp or time.time()
        # what do you know about this thing? who is the recepient?

    @property
    def calculate_hash(self):
        block_of_string = "{}{}{}{}{}".format(self.index, self.proof_no,
                                              self.prev_hash, self.data,
                                              self.timestamp)

        return hashlib.sha256(block_of_string.encode()).hexdigest()

    def __repr__(self):
        # index, proof_no, prev_hash, data, timestamp
        # can you alter the data?
        return "{} - {} - {} - {} - {}".format(self.index, self.proof_no,
                                               self.prev_hash, self.data,
                                               self.timestamp)


class BlockChain:

    def __init__(self, string=None):
        if string is not None:
            self.load(string)
        else:
            self.chain = []
            self.current_data = []
            self.nodes = set()
            self.construct_genesis()
        # you shall be not using this shit, if the struct is somehow ready
    def dump(self):
        content = {"chain":self.chain,
                "current_data":self.current_data,
                "nodes":self.nodes}
        content = json.dumps(content)
        return content

    def load(self, string):
        content = json.loads(string)
        self.chain = content["chain"]
        self.current_data = content["current_data"]
        self.nodes = content["nodes"]

    def construct_genesis(self):
        self.construct_block(proof_no=0, prev_hash=0)

    def construct_block(self, proof_no, prev_hash):
        block = Block(
            index=len(self.chain),
            proof_no=proof_no,
            prev_hash=prev_hash,
            data=self.current_data)
        self.current_data = []

        self.chain.append(block)
        return block
# every time there is something new.
# i guess the security is not strictly an issue, since we are dealing with pretty much idiot-like bots.
# you are right. so let's not fuck with bitcoin or anything like that.
    @staticmethod
    def check_validity(block, prev_block):
        if prev_block.index + 1 != block.index:
            return False

        elif prev_block.calculate_hash != block.prev_hash:
            return False

        elif not BlockChain.verifying_proof(block.proof_no,
                                            prev_block.proof_no):
            return False

        elif block.timestamp <= prev_block.timestamp:
            return False

        return True

    def new_data(self, sender, recipient, quantity):
        self.current_data.append({
            'sender': sender,
            'recipient': recipient,
            'quantity': quantity
        })
        return True
    def check_format(self,data):
        try:
            spk, rpk = data["sender"], data["recipient"]
            amount = data["quantity"]
            assert list(data.keys()) == ["sender", "recipient", "quantity"]
            assert amount>0
            spk, rpk = get_pubkey(spk), get_pubkey(rpk)
            return True
        except:
            return False
    def check_account(self,data):
        sender = data["sender"]
        sender_pubkey = sender
        sender = self.get_all_accounts(targets=[sender_pubkey])
        remaining = sender[sender_pubkey]
        if remaining<0:
            return False
        amount = data["quantity"]
        return amount < remaining
    def transaction(self, content):
        try:
            content = json.loads(content)
            data = content["data"]
            # first check the format.
            action = json.loads(data)
            if not check_format(action):
                print("incorrect format.")
                return
            pub_key = action["sender"]
            pub_key = get_pubkey(pub_key)
            sign = content["sign"]
            r,s = sign["r"], sign["s"]
            valid = verify_message(data,pub_key,r,s)
            if valid:
                print("valid transaction: {}".format(data))
                error = check_account(action)
                # we cannot afford debt!
                if not error:
                    # perform_action
                    new_data(action["sender"],action["recipient"],action["quantity"])
                else:
                    print("can't perform transaction due to error: {}".format(error))
            else:
                print("invalid transaction: {}".format(data))
                return
        except:
            traceback.print_exc()
            print("error when performing transaction.")

    @staticmethod
    def proof_of_work(last_proof):
        '''this simple algorithm identifies a number f' such that hash(ff') contain 4 leading zeroes
         f is the previous f'
         f' is the new proof
        '''
        proof_no = 0
        while BlockChain.verifying_proof(proof_no, last_proof) is False:
            proof_no += 1
            # will you exceed the limit?

        return proof_no

    @staticmethod
    def verifying_proof(last_proof, proof):
        #verifying the proof: does hash(last_proof, proof) contain 4 leading zeroes?

        guess = f'{last_proof}{proof}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()
        return guess_hash[:4] == "0000"

    @property
    def latest_block(self):
        return self.chain[-1]

    def block_mining(self, details_miner):

        self.new_data(
            sender="0",  #it implies that this node has created a new block
            recipient=details_miner,
            quantity=
            award_amount,  #creating a new block (or identifying the proof number) is awarded with 1
        )

        last_block = self.latest_block

        last_proof_no = last_block.proof_no
        proof_no = self.proof_of_work(last_proof_no)

        last_hash = last_block.calculate_hash
        block = self.construct_block(proof_no, last_hash)

        return vars(block)
# what is this vars?
    def create_node(self, address):
        self.nodes.add(address)
        return True

    @staticmethod
    def obtain_block_object(block_data):
        #obtains block object from the block data

        return Block(
            block_data['index'],
            block_data['proof_no'],
            block_data['prev_hash'],
            block_data['data'],
            timestamp=block_data['timestamp'])
# how do you send shits?
    def get_all_accounts(self,targets=None):
        all_data = [y for x in self.chain for y in x.data]
        senders = [x["sender"] for x in all_data]
        recipients = [x["recipient"] for x in all_data]
        amounts = [x["quantity"] for x in all_data]
        all_accounts = set(senders+recipients)
        if targets is None:
            final_result = {x:0 for x in all_accounts}
            for x in range(len(senders)):
                final_result[senders[x]]-=amounts[x]
                final_result[recipients[x]]+=amounts[x]
            return final_result
        else:
            assert type(targets) == list
            final_result = {x:0 for x in all_accounts if x in targets}
            for x in targets:
                for index, y in enumerate(senders):
                    if y == x:
                        final_result[x]-=amounts[index]
                for index, y in enumerate(recipients):
                    if y == x:
                        final_result[x]+=amounts[index]
            return final_result

blockchain = BlockChain()
# you shall be able to load and dump this chain.
